package jmine.tec.web.servlet.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import jmine.tec.utils.spring.SpringBeanReference;
import jmine.tec.web.servlet.filter.transaction.DefaultTransactionAttributeFactory;
import jmine.tec.web.servlet.filter.transaction.TransactionDefinitionFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import bancosys.tec.persist.audit.AuditManager;

/**
 * Gerencia transacoes usando um {@link PlatformTransactionManager} definido no {@link WebApplicationContext}.
 * 
 * @author takeshi
 */
public class TransactionFilter implements Filter {

    private PlatformTransactionManager platformTransactionManager;

    private TransactionDefinitionFactory transactionDefinitionFactory;

    private String transactionManagerBeanName = "transactionManager",
            transactionDefinitionFactoryBeanName = "transactionDefinitionFactory";

    private FilterConfig filterCfg;

    private Log logger = LogFactory.getLog(TransactionFilter.class);
    
    private static final ThreadLocal<TransactionStatus> TX_STATUS = new ThreadLocal<TransactionStatus>();

    private static final Runnable EMPTY_RUNNABLE = new Runnable() {
        public void run() {
        }
    };

    /**
     * @return {@link FilterConfig}
     */
    protected final FilterConfig getFilterCfg() {
        return this.filterCfg;
    }

    /**
     * Metodo para subclasses sobrescreverem
     * 
     * @param webContext {@link WebApplicationContext}
     */
    protected void doInitialize(WebApplicationContext webContext) {
    }

    private Runnable initializer = new Runnable() {
        public synchronized void run() {
            if (TransactionFilter.this.platformTransactionManager == null && TransactionFilter.this.transactionDefinitionFactory == null) {
                WebApplicationContext webApplicationContext =
                        WebApplicationContextUtils.getWebApplicationContext(TransactionFilter.this.filterCfg.getServletContext());
                TransactionFilter.this.doInitialize(webApplicationContext);
                TransactionFilter.this.platformTransactionManager =
                        (PlatformTransactionManager) webApplicationContext.getBean(TransactionFilter.this.transactionManagerBeanName);
                if (webApplicationContext.containsBean(TransactionFilter.this.transactionDefinitionFactoryBeanName)) {
                    TransactionFilter.this.transactionDefinitionFactory =
                            (TransactionDefinitionFactory) webApplicationContext
                                    .getBean(TransactionFilter.this.transactionDefinitionFactoryBeanName);
                } else {
                    TransactionFilter.this.transactionDefinitionFactory = new DefaultTransactionAttributeFactory();
                }
            }
            TransactionFilter.this.initializer = TransactionFilter.EMPTY_RUNNABLE;
        }
    };

    /**
     * 
     */
    public void destroy() {
    }

    /**
     * @param request the servlet request
     * @param response the servlet response
     * @param chain the invocation chain
     * @throws IOException e
     * @throws ServletException e
     */
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException,
            ServletException {
        this.initializer.run();
        ListableBeanFactory previousBeanFactory =
                SpringBeanReference.setThreadLocalBeanFactory(WebApplicationContextUtils.getWebApplicationContext(this.filterCfg
                        .getServletContext()));
        TransactionStatus tx = this.platformTransactionManager.getTransaction(this.transactionDefinitionFactory.create(request));
        TX_STATUS.set(tx);
        String ipAddress = getIpAddressClient(request);

        AuditManager.setAuditSource("<web@" + ipAddress + ">");
        try {
            chain.doFilter(request, response);
            if (!tx.isCompleted()) {
                if (tx.isRollbackOnly()) {
                    this.platformTransactionManager.rollback(tx);
                } else {
                    this.platformTransactionManager.commit(tx);
                }
            }
        } catch (IOException e) {
            this.logger.error(e);
            if (!tx.isCompleted()) {
                this.platformTransactionManager.rollback(tx);
            }
            throw e;
        } catch (ServletException e) {
            this.logger.error(e);
            if (!tx.isCompleted()) {
                this.platformTransactionManager.rollback(tx);
            }
            throw e;
        } catch (RuntimeException e) {
            this.logger.error(e);
            if (!tx.isCompleted()) {
                this.platformTransactionManager.rollback(tx);
            }
            throw e;
        } catch (Error e) {
            this.logger.error(e);
            if (!tx.isCompleted()) {
                this.platformTransactionManager.rollback(tx);
            }
            throw e;
        } finally {
            SpringBeanReference.setThreadLocalBeanFactory(previousBeanFactory);
            TX_STATUS.remove();
            AuditManager.setAuditSource(null);
        }
    }

    /**
     * @param filterConfig the filter
     * @throws ServletException e
     */
    public void init(final FilterConfig filterConfig) throws ServletException {
        this.filterCfg = filterConfig;
        if (filterConfig.getInitParameter("transactionManagerBeanName") != null) {
            this.transactionManagerBeanName = filterConfig.getInitParameter("transactionManagerBeanName");
        }
        if (filterConfig.getInitParameter("transactionFactoryBeanName") != null) {
            this.transactionDefinitionFactoryBeanName = filterConfig.getInitParameter("transactionFactoryBeanName");
        }
    }

    /**
     * Marca a transacao atual como rollbackOnly
     * 
     * @return boolean
     */
    public static boolean markCurrentTransactionRollbackOnly() {
        TransactionStatus status = TX_STATUS.get();
        if (status == null) {
            return false;
        } else if (!status.isRollbackOnly()) {
            status.setRollbackOnly();
        }
        return true;
    }

    /**
     * @return the platformTransactionManager
     */
    public PlatformTransactionManager getPlatformTransactionManager() {
        return this.platformTransactionManager;
    }

    /**
     * @param platformTransactionManager the platformTransactionManager to set
     */
    public void setPlatformTransactionManager(PlatformTransactionManager platformTransactionManager) {
        this.platformTransactionManager = platformTransactionManager;
    }

    /**
     * @param transactionDefinitionFactory the transactionDefinitionFactory to set
     */
    public void setTransactionDefinitionFactory(TransactionDefinitionFactory transactionDefinitionFactory) {
        this.transactionDefinitionFactory = transactionDefinitionFactory;
    }
    
    /**
     * Este método é chamado por doFilter para processar uma solicitação GET.
     * 
     * @param req ServletRequest request
     * @return ipAddress endereço IP do cliente
     * @throws IOException IOException
     */
    private String getIpAddressClient(ServletRequest req) throws IOException {
        String ipAddress = req.getParameter("x-forwarded-for");
        if (ipAddress == null) {
            ipAddress = req.getParameter("X_FORWARDED_FOR");
            if (ipAddress == null) {
                ipAddress = req.getRemoteHost();
                if (ipAddress == null) {
                    ipAddress = req.getRemoteAddr();
                }
            }
        }
        return ipAddress;
    }
}
